id, $pages, true) && strpos($screen->id, 'im-candidates') === false && strpos($screen->id, 'im-candidate-detail') === false) return; ?> '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if ($c && $c->status === 'screening') { IM_Candidate::update_status($id, 'invited'); } IM_Mailer::send_interview_invite($id) ? wp_send_json_success(['message' => '面试邀请邮件已成功发送!']) : wp_send_json_error(['message' => '邮件发送失败,请检查邮件配置。']); }); add_action('wp_ajax_im_action_reject', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Candidate::update_status($id, 'rejected'); IM_Mailer::send_reject($id) ? wp_send_json_success(['message' => '已拒绝该候选人并发送邮件!']) : wp_send_json_error(['message' => '已更新状态,但邮件发送失败。']); }); add_action('wp_ajax_im_action_hire', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => '已录取该候选人并发送培训链接邮件!']) : wp_send_json_error(['message' => '录取操作失败,请检查邮件配置。']); }); add_action('wp_ajax_im_action_resend_joinus', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Mailer::send_joinus_confirmation($id) ? wp_send_json_success(['message' => '表单链接邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_resend_training', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'training') wp_send_json_error(['message' => '该候选人不在培训状态']); IM_Mailer::send_hire($id) ? wp_send_json_success(['message' => '培训链接邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_resend_trained_email', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); $c = IM_Candidate::get($id); if (!$c || $c->status !== 'trained') wp_send_json_error(['message' => '该候选人不在已完成培训状态']); IM_Mailer::send_training_complete($id) ? wp_send_json_success(['message' => '账号邮件已重新发送!']) : wp_send_json_error(['message' => '邮件发送失败。']); }); add_action('wp_ajax_im_action_delete', function () { check_ajax_referer('im_admin_nonce', 'nonce'); if (!current_user_can('edit_others_posts')) wp_send_json_error(['message' => '权限不足']); $id = (int) ($_POST['candidate_id'] ?? 0); if (!$id) wp_send_json_error(['message' => '参数错误']); IM_Candidate::delete_with_files($id) ? wp_send_json_success(['message' => '候选人记录及关联文件已删除!']) : wp_send_json_error(['message' => '删除失败,请重试。']); }); /* ============================================================ 候选人列表页 ============================================================ */ function im_admin_list() { $page = max(1, (int) ($_GET['paged'] ?? 1)); $status = sanitize_text_field($_GET['status'] ?? ''); $search = sanitize_text_field($_GET['s'] ?? ''); $per_page = 20; $args = compact('page', 'status', 'search') + ['per_page' => $per_page]; $items = IM_Candidate::get_list($args); $total = IM_Candidate::count($args); $pages = (int) ceil($total / $per_page); $cnt = [ '' => IM_Candidate::count(), 'applied' => IM_Candidate::count(['status' => 'applied']), 'screening' => IM_Candidate::count(['status' => 'screening']), 'invited' => IM_Candidate::count(['status' => 'invited']), 'rejected' => IM_Candidate::count(['status' => 'rejected']), 'completed' => IM_Candidate::count(['status' => 'completed']), 'hired' => IM_Candidate::count(['status' => 'hired']), 'training' => IM_Candidate::count(['status' => 'training']), 'trained' => IM_Candidate::count(['status' => 'trained']), ]; $tabs = [ '' => '全部', 'applied' => '已申请', 'screening' => '待筛选', 'invited' => '已邀请', 'rejected' => '已拒绝', 'completed' => '已完成', 'hired' => '已录取', 'training' => '未完成培训', 'trained' => '已完成培训' ]; $status_lbl = [ 'applied' => '待完善信息', 'screening' => '已提交详细信息', 'invited' => '已邀请', 'rejected' => '已拒绝', 'completed' => '已完成', 'hired' => '已录取', 'training' => '未完成培训', 'trained' => '已完成培训' ]; $colors = [ 'applied' => '#f59e0b', 'screening' => '#8b5cf6', 'invited' => '#3b82f6', 'rejected' => '#ef4444', 'completed' => '#10b981', 'hired' => '#059669', 'training' => '#f97316', 'trained' => '#06b6d4' ]; ?>

候选人管理


id); $subs = IM_Candidate::get_subjects($c); $color = $colors[$c->status] ?? '#6b7280'; $clabel = $status_lbl[$c->status] ?? $c->status; $name_disp = $c->status === 'applied' ? esc_html($c->last_name . ' ' . $c->first_name) : '' . esc_html($c->last_name . ' ' . $c->first_name) . ''; ?>
姓名 邮箱 更新时间 状态 授课科目 / 详情 操作
preferred_name): ?> (preferred_name) ?>) email) ?> updated_at))) ?> status === 'applied' && !empty($c->apply_opened_at)): ?>
✓ 已打开表单
status === 'applied' && empty($c->apply_opened_at)): ?>
○ 未打开表单
status === 'invited'): ?> id); if (!empty($tk_list)): $tk = $tk_list[0]; ?> opened_at)): ?>
✓ 已查看面试题
○ 未点开面试链接
status === 'training'): ?> training_opened_at)): ?>
✓ 已打开培训页面
○ 未打开培训页面
status === 'trained'): ?> training_completed_at)): ?>
training_completed_at)) ?> 完成
status === 'applied'): ?> 等待完善信息 3): ?>+ status !== 'applied'): ?> 查看详情 status === 'applied'): ?> status === 'screening'): ?> status === 'invited'): ?> status === 'completed'): ?> status === 'training'): ?> status === 'trained'): ?>
apply_token_used) && $c->apply_token): ?> status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): $latest_token = IM_Token::get_by_candidate($c->id); if (!empty($latest_token)): $tk2 = $latest_token[0]; ?>
调试: 面试链接
token, IM_INTERVIEW_PAGE_URL)) ?>
status, ['training', 'trained']) && !empty($c->training_token)): ?>
暂无候选人数据
1): $base = admin_url('admin.php?page=im-candidates' . ($status ? '&status=' . $status : '') . ($search ? '&s=' . urlencode($search) : '')); ?>

候选人不存在。

'; return; } if ($c->status === 'applied') { echo '

该候选人尚未提交详细申请表,无法查看详情。

返回列表
'; return; } $subjects = IM_Candidate::get_subjects($c); $tokens = IM_Token::get_by_candidate($id); $attachments = IM_Attachment::get_by_candidate($id); $upload_dir = wp_upload_dir(); $status_lbl = [ 'applied' => '待完善信息', 'screening' => '已提交详细信息', 'invited' => '已邀请', 'rejected' => '已拒绝', 'completed' => '已完成', 'hired' => '已录取', 'training' => '未完成培训', 'trained' => '已完成培训' ]; $status_colors = [ 'applied' => ['bg' => '#fffbeb', 'text' => '#b45309', 'border' => '#fde68a'], 'screening' => ['bg' => '#f3e8ff', 'text' => '#6d28d9', 'border' => '#e9d5ff'], 'invited' => ['bg' => '#eff6ff', 'text' => '#1d4ed8', 'border' => '#bfdbfe'], 'rejected' => ['bg' => '#fef2f2', 'text' => '#b91c1c', 'border' => '#fecaca'], 'completed' => ['bg' => '#ecfdf5', 'text' => '#047857', 'border' => '#a7f3d0'], 'hired' => ['bg' => '#ecfccb', 'text' => '#166534', 'border' => '#d9f99d'], 'training' => ['bg' => '#fff7ed', 'text' => '#c2410c', 'border' => '#fed7aa'], 'trained' => ['bg' => '#ecfeff', 'text' => '#0e7490', 'border' => '#a5f3fc'] ]; $full_name = esc_html($c->first_name . ' ' . $c->last_name); $p_name = esc_html($c->preferred_name); $initials = mb_substr(trim($c->first_name), 0, 1) . mb_substr(trim($c->last_name), 0, 1); if (!$initials && $full_name) $initials = mb_substr($full_name, 0, 1); $s_col = $status_colors[$c->status]; ?>
返回候选人列表

(' . $p_name . ')' : '' ?>

email) ?> phone): ?> phone) ?> country) || !empty($c->city)): ?> city ?? '') . (!empty($c->city) && !empty($c->country) ? ', ' : '') . ($c->country ?? ''))) ?>
status] ?>

教育背景

'本科', "Master's" => '硕士', 'PhD' => '博士']; ?>
grad_year ?: '未知年份') ?> 毕业 (degree_level] ?? ($c->degree_level ?: '最高学历')) ?>)

university ?: '—') ?>

专业:major ?: '—') ?>  |  GPA:gpa ?: '—') ?> deans_list ? ' |  🏅 Dean\'s List' : '' ?>
ug_university): ?>
ug_grad_year ?: '未知年份') ?> 毕业 (本科学历)

ug_university) ?>

专业:ug_major ?: '—') ?>
ms_university)): ?>
ms_grad_year ?: '未知年份') ?> 毕业 (硕士学历)

ms_university) ?>

专业:ms_major ?: '—') ?>
ca_highschool): ?>
Canadian High School

ca_highschool_name ?: '—') ?>

e.g. OSSD or BC curriculum

技能与经验

授课科目 (个)
未选择任何科目
英语流利度
'Native (母语)', 'Fluent' => 'Fluent (流利)', 'Basic' => 'Basic (基础)']; echo esc_html($fluency_map[$c->languages] ?? ($c->languages ?: '—')); ?>
教学经验
teaching_exp ?: '—')) ?>
has_achievement): ?>
个人成就 (achievement_type) ?>)
achievement_desc ?: '—')) ?>
extra_notes): ?>
补充说明
extra_notes)) ?>
status === 'screening'): ?>

审核操作

请决定是否邀请该候选人进行面试

status === 'invited'): ?>

等待面试中

如果候选人未收到邮件可重新发送

status === 'completed'): ?>

面试审查

视频已提交,请查看并在通过后录用

status === 'training'): ?>

培训进行中

候选人正在完成培训课程

training_opened_at)): ?>
✓ 已于 training_opened_at)) ?> 打开培训页面
○ 尚未打开培训页面
status === 'trained'): ?>

培训已完成

候选人已通过所有培训模块

training_completed_at)): ?>
✓ 于 training_completed_at)) ?> 完成培训

附件材料

file_type][] = $att; $att_groups = ['transcript_files' => '成绩单', 'achievement_files' => '成就证明', 'extra_files' => '补充材料']; foreach ($att_groups as $type => $label): if (empty($grouped[$type])) continue; ?>
id . '/' . basename($att->file_path); ?>
file_name) ?>
status, ['invited', 'completed', 'hired', 'rejected', 'training', 'trained'])): ?>

面试记录

is_used; $expired = strtotime($tk->expires_at) < time(); if ($used) { $bs = '#dcfce7'; $bc = '#86efac'; $bt = '#166534'; $bl = '已提交视频'; } elseif ($expired) { $bs = '#fee2e2'; $bc = '#fca5a5'; $bt = '#991b1b'; $bl = '链接已过期'; } else { $bs = '#dbeafe'; $bc = '#93c5fd'; $bt = '#1e40af'; $bl = '有效中'; } ?>
created_at)) ?> 发送
expires_at)) ?> 失效
opened_at)): ?>
✓ 于 opened_at)) ?> 查看题目
○ 候选人未打开链接
video_path && file_exists($tk->video_path)): $vurl = $upload_dir['baseurl'] . '/interviews/' . $c->id . '/' . $tk->video_filename; $fext = strtolower(pathinfo($tk->video_filename, PATHINFO_EXTENSION)); $archive_exts = ['zip', 'rar', '7z', 'gz', 'tar']; $fsize = filesize($tk->video_path); $fsize_str = $fsize > 1048576 ? round($fsize / 1048576, 1) . ' MB' : round($fsize / 1024, 1) . ' KB'; ?>
video_filename) ?>
压缩包 ·
下载
submitted_at)) ?> 提交

视频已脱机或不存在

尚未发送面试邀请
= 400 * 1024 * 1024, '建议设为 500M'], ['post_max_size', ini_get('post_max_size'), wp_convert_hr_to_bytes(ini_get('post_max_size')) >= 400 * 1024 * 1024, '建议设为 512M'], ['max_execution_time', ini_get('max_execution_time') . 's', (int) ini_get('max_execution_time') === 0 || (int) ini_get('max_execution_time') >= 180, '建议设为 300'], ['memory_limit', ini_get('memory_limit'), true, '—'], ]; ?>

Interview Manager — 服务器配置向导

① Nginx 配置(必须手动完成)

server { } 块内添加以下配置,然后执行 sudo nginx -t && sudo nginx -s reload

# 视频上传限制
            client_max_body_size 512M;
            client_body_timeout  300s;
            send_timeout         300s;

            # WordPress 固定链接(已有则跳过)
            location / {
                try_files $uri $uri/ /index.php?$args;
            }

            # 上传目录安全
            location ~* ^/wp-content/uploads/(interviews|im-applications)/ {
                add_header X-Content-Type-Options nosniff;
            }

② PHP 配置(php.ini 或 .user.ini)

upload_max_filesize = 500M
            post_max_size       = 512M
            max_execution_time  = 300
            max_input_time      = 300
            memory_limit        = 256M

修改后重启 PHP-FPM:sudo systemctl restart php8.x-fpm

③ 刷新 WordPress 固定链接

进入 后台 → 设置 → 固定链接,直接点击「保存更改」即可(无需改任何设置)。

前往固定链接设置 →

④ WordPress 页面配置

需在 WordPress 后台创建以下两个页面:

页面 Shortcode 建议 URL slug
Join Us 报名页 [im_joinus_form] /join-us/
详细申请表单页 [im_apply_form] /apply/
面试页 [im_interview] /interview/
培训页 [im_training] /training/

创建好页面后,在片段 1 顶部将对应常量改为实际 URL:

define('IM_APPLY_PAGE_URL', home_url('/apply/'));
define('IM_INTERVIEW_PAGE_URL', home_url('/interview/'));
define('IM_TRAINING_PAGE_URL', home_url('/training/'));
define('IM_TRAINING_ACCOUNT', 'your_account@example.com');
define('IM_TRAINING_PASSWORD', 'your_password');

⑤ 当前环境检测

配置项 当前值 状态
视频存储目录
申请文件目录
面试页面 URL
培训页面 URL
培训临时账号